在 GASO 專案的開發過程中,我們不斷地在功能完整性和使用者體驗之間尋找平衡點。經過二十六天的開發,我們已經建立了一個功能豐富的學習地圖平台,但隨著功能的增加,介面也變得越來越複雜。
今天,我們將專注於一個重要的設計原則:簡化。有時候,移除不必要的功能比添加新功能更能提升使用者體驗。讓我們一起探索如何通過簡化來優化 GASO 的使用者體驗。
今天最重要的改進其實不在程式碼層面,而是在內容層面。我完成了 65個節點的完整提示詞撰寫,這是 GASO 專案的核心價值所在。
每個提示詞都經過精心設計,包含以下要素:
這 65 個提示詞代表了:
在之前的版本中,當使用者點擊節點時,彈出視窗會顯示大量的技術資訊:
<!-- 之前的複雜顯示 -->
<div class="info-row">
  <span class="info-label">ID:</span>
  <span class="info-value">${nodeDetail.id}</span>
</div>
<div class="info-row">
  <span class="info-label">屬性:</span>
  <span class="info-value">${nodeDetail.attribute}</span>
</div>
<div class="info-row">
  <span class="info-label">座標:</span>
  <span class="info-value">X: ${nodeCoordinates.x.toFixed(2)}, Y: ${nodeCoordinates.y.toFixed(2)}</span>
</div>
<div class="info-row">
  <span class="info-label">個人化:</span>
  <span class="info-value">已套用您的個人資訊</span>
</div>
這些資訊對一般使用者來說:
我們將節點資訊簡化為只顯示最重要的內容:
<!-- 簡化後的顯示 -->
<div class="node-info">
  <div class="info-row">
    <span class="info-label">提示:</span>
    <div class="info-prompt">
      ${fullPrompt.replace(/\n/g, '<br>')}
      <button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
    </div>
  </div>
</div>
在之前的版本中,複製按鈕使用 onclick 屬性直接綁定事件:
<!-- 之前的實現方式 -->
<button class="copy-button" onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')">複製</button>
這種方式存在以下問題:
我們改用 data 屬性和事件委託的方式:
<!-- 改善後的實現方式 -->
<button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
// 在 showCustomModal 函數中添加事件監聽器
const copyButton = content.querySelector('.copy-button');
if (copyButton) {
  copyButton.addEventListener('click', function() {
    const promptText = this.getAttribute('data-prompt');
    copyPromptToClipboard(promptText);
  });
}
我們將 showCustomModal 函數的職責更加明確:
function showCustomModal(nodeDetail) {
  console.log("顯示節點詳情:", nodeDetail);
  const modal = document.getElementById('nodeModal');
  const title = document.getElementById('modalTitle');
  const content = document.getElementById('modalContent');
  
  // 生成客製化的 prompt
  const customPrefix = generateCustomPromptPrefix();
  const fullPrompt = customPrefix + nodeDetail.prompt;
  
  // 設定標題
  title.textContent = `${nodeDetail.label}`;
  
  // 設定內容(簡化版)
  content.innerHTML = `
    <div class="node-info">
      <div class="info-row">
        <span class="info-label">提示:</span>
        <div class="info-prompt">
          ${fullPrompt.replace(/\n/g, '<br>')}
          <button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
        </div>
      </div>
    </div>
  `;
  
  // 顯示彈出視窗
  modal.style.display = 'block';
  
  // 綁定事件監聽器
  bindCopyButtonEvent(content);
}
我們將事件綁定邏輯提取為獨立的函數:
function bindCopyButtonEvent(content) {
  const copyButton = content.querySelector('.copy-button');
  if (copyButton) {
    copyButton.addEventListener('click', function() {
      const promptText = this.getAttribute('data-prompt');
      copyPromptToClipboard(promptText);
    });
  }
}
在 GASO 專案中,提示詞不僅僅是技術實現的一部分,更是整個學習體驗的核心。沒有高品質的提示詞,再好的介面設計也沒有意義。
// 提示詞的價值鏈
const valueChain = {
  content: "高品質的提示詞",      // 核心價值
  interface: "簡潔的介面設計",    // 價值傳遞
  experience: "優質的學習體驗"    // 最終目標
};
撰寫 65 個節點的提示詞面臨以下挑戰:
每個提示詞都遵循以下結構:
## 提示詞結構模板
### 1. 明確的學習目標
"請教導如何使用 Google Apps Script 自動化 Google Calendar 操作"
### 2. 結構化的學習內容
1. Calendar API 的基本概念
2. 如何建立、修改和刪除事件
3. 行事曆的查詢和篩選
4. 重複事件的處理
5. 提醒和通知設定
6. 實際的應用場景範例
7. 權限管理和安全性考量
### 3. 具體的學習要求
"請提供完整的程式碼範例和實用案例"
每個提示詞都必須符合以下標準:
// 提示詞的資料結構
const promptStructure = {
  nodeId: "Ca",                    // 節點唯一識別碼
  nodeLabel: "Google Calendar 自動化", // 節點顯示名稱
  prompt: `請教導如何使用 Google Apps Script 自動化 Google Calendar 操作:
1. Calendar API 的基本概念
2. 如何建立、修改和刪除事件
3. 行事曆的查詢和篩選
4. 重複事件的處理
5. 提醒和通知設定
6. 實際的應用場景範例
7. 權限管理和安全性考量
請提供完整的程式碼範例和實用案例。`
};
// 提示詞與個人化資訊的整合
function generateCustomPromptPrefix() {
  const userInfo = state.userInfo;
  if (userInfo.role && userInfo.title && userInfo.gasLevel) {
    return `我是${userInfo.role}的${userInfo.title},我對 Google Apps Script 熟悉的程度是${userInfo.gasLevel}。`;
  }
  return "";
}
// 完整的提示詞生成
function generateFullPrompt(nodeDetail) {
  const customPrefix = generateCustomPromptPrefix();
  return customPrefix + nodeDetail.prompt;
}
// 提示詞的持續改進
const promptImprovement = {
  collectFeedback() {
    // 收集使用者對提示詞的回饋
  },
  
  analyzeEffectiveness() {
    // 分析提示詞的學習效果
  },
  
  updatePrompts() {
    // 根據回饋更新提示詞
  }
};
我們移除了以下不必要的資訊:
對於必要的功能,我們進行了簡化:
在簡化的基礎上,我們優化了使用者體驗:
// 問自己:使用者真的需要這個資訊嗎?
if (userNeedsThisInfo) {
  // 保留
} else {
  // 移除
}
// 問自己:這個功能對核心目標有幫助嗎?
if (helpsCoreGoal) {
  // 保留並優化
} else {
  // 移除或簡化
}
// 問自己:這個功能容易維護嗎?
if (easyToMaintain) {
  // 保留
} else {
  // 重構或移除
}
// 之前的複雜轉義
onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')"
// 使用 HTML 實體,更安全且更清晰
data-prompt='${fullPrompt.replace(/'/g, "'")}'
<!-- 內聯事件處理的問題 -->
<button onclick="someFunction()">按鈕</button>
// 事件委託的優點
element.addEventListener('click', function() {
  // 事件處理邏輯
});
<!-- 過多的 DOM 元素 -->
<div class="info-row">
  <span class="info-label">ID:</span>
  <span class="info-value">...</span>
</div>
<div class="info-row">
  <span class="info-label">屬性:</span>
  <span class="info-value">...</span>
</div>
<!-- 更多不必要的元素 -->
<!-- 精簡的 DOM 結構 -->
<div class="node-info">
  <div class="info-row">
    <span class="info-label">提示:</span>
    <div class="info-prompt">
      <!-- 只保留必要的內容 -->
    </div>
  </div>
</div>
使用者點擊節點後看到:
使用者點擊節點後只看到:
┌─────────────────────────┐
│ 節點名稱                │
├─────────────────────────┤
│ ID: node_123            │
│ 屬性: learning          │
│ 座標: X: 123.45, Y: 67.89│
│ 個人化: 已套用          │
│ 提示: 學習內容...       │
│ [複製]                  │
└─────────────────────────┘
┌─────────────────────────┐
│ 節點名稱                │
├─────────────────────────┤
│ 提示: 學習內容...       │
│ [複製]                  │
└─────────────────────────┘
<div class="node-info">
  <div class="info-row">...</div>  <!-- ID -->
  <div class="info-row">...</div>  <!-- 屬性 -->
  <div class="info-row">...</div>  <!-- 座標 -->
  <div class="info-row">...</div>  <!-- 個人化 -->
  <div class="info-row">...</div>  <!-- 提示 -->
</div>
<div class="node-info">
  <div class="info-row">...</div>  <!-- 提示 -->
</div>
功能數量 ↑ = 產品價值 ↑
功能精準度 ↑ = 使用者滿意度 ↑
function calculateUserValue(feature) {
  const userNeed = feature.solvesUserProblem;
  const frequency = feature.usageFrequency;
  const satisfaction = feature.userSatisfaction;
  
  return userNeed * frequency * satisfaction;
}
function calculateMaintenanceCost(feature) {
  const complexity = feature.codeComplexity;
  const bugs = feature.bugFrequency;
  const updates = feature.updateFrequency;
  
  return complexity * bugs * updates;
}
function alignsWithCoreGoal(feature) {
  const coreGoal = "幫助使用者學習 Google Apps Script";
  return feature.contributesTo(coreGoal);
}
// 不是這樣:
removeAllFeatures();
// 而是這樣:
focusOnCoreFeatures();
// 不是這樣:
// 懶得實作,所以移除
// 而是這樣:
// 經過深思熟慮,決定移除
## 功能清單
- [ ] 節點 ID 顯示
- [ ] 節點屬性顯示
- [ ] 座標資訊顯示
- [ ] 個人化狀態顯示
- [ ] 提示內容顯示
- [ ] 複製功能
## 功能評估
- [ ] 節點 ID 顯示 - 使用者不需要 ❌
- [ ] 節點屬性顯示 - 內部資訊 ❌
- [ ] 座標資訊顯示 - 開發者資訊 ❌
- [ ] 個人化狀態顯示 - 系統狀態 ❌
- [ ] 提示內容顯示 - 核心功能 ✅
- [ ] 複製功能 - 核心功能 ✅
## 最終決定
- [x] 提示內容顯示 - 保留並優化
- [x] 複製功能 - 保留並改善
- [ ] 其他功能 - 移除
// 測試簡化前後的版本
const versionA = "複雜版本";
const versionB = "簡化版本";
// 比較使用者行為
compareUserBehavior(versionA, versionB);
// 詢問使用者對簡化的看法
const questions = [
  "你覺得這個介面如何?",
  "你覺得哪些資訊是必要的?",
  "你覺得哪些資訊是多餘的?"
];
interviewUsers(questions);
// 移除技術細節
removeTechnicalDetails();
// 簡化事件處理
simplifyEventHandling();
// 優化互動流程
optimizeUserInteraction();
function showCustomModal(nodeDetail) {
  // 大量複雜的邏輯
  const customPrefix = generateCustomPromptPrefix();
  const fullPrompt = customPrefix + nodeDetail.prompt;
  const nodeCoordinates = getNodeCoordinates(nodeDetail.id);
  
  // 複雜的 HTML 生成
  content.innerHTML = `
    <div class="node-info">
      <div class="info-row">
        <span class="info-label">ID:</span>
        <span class="info-value">${nodeDetail.id}</span>
      </div>
      ${nodeDetail.attribute ? `
      <div class="info-row">
        <span class="info-label">屬性:</span>
        <span class="info-value">${nodeDetail.attribute}</span>
      </div>
      ` : ''}
      ${nodeCoordinates ? `
      <div class="info-row">
        <span class="info-label">座標:</span>
        <span class="info-value">X: ${nodeCoordinates.x.toFixed(2)}, Y: ${nodeCoordinates.y.toFixed(2)}</span>
      </div>
      ` : ''}
      <div class="info-row">
        <span class="info-label">提示:</span>
        <div class="info-prompt">
          ${fullPrompt.replace(/\n/g, '<br>')}
          <button class="copy-button" onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')">複製</button>
        </div>
      </div>
      ${state.userInfo.role && state.userInfo.title && state.userInfo.gasLevel ? `
      <div class="info-row">
        <span class="info-label">個人化:</span>
        <span class="info-value">已套用您的個人資訊</span>
      </div>
      ` : ''}
    </div>
  `;
}
function showCustomModal(nodeDetail) {
  const modal = document.getElementById('nodeModal');
  const title = document.getElementById('modalTitle');
  const content = document.getElementById('modalContent');
  
  // 生成客製化的 prompt
  const customPrefix = generateCustomPromptPrefix();
  const fullPrompt = customPrefix + nodeDetail.prompt;
  
  // 設定標題和內容
  title.textContent = `${nodeDetail.label}`;
  content.innerHTML = generateModalContent(fullPrompt);
  
  // 顯示彈出視窗並綁定事件
  modal.style.display = 'block';
  bindCopyButtonEvent(content);
}
function generateModalContent(fullPrompt) {
  return `
    <div class="node-info">
      <div class="info-row">
        <span class="info-label">提示:</span>
        <div class="info-prompt">
          ${fullPrompt.replace(/\n/g, '<br>')}
          <button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
        </div>
      </div>
    </div>
  `;
}
function bindCopyButtonEvent(content) {
  const copyButton = content.querySelector('.copy-button');
  if (copyButton) {
    copyButton.addEventListener('click', function() {
      const promptText = this.getAttribute('data-prompt');
      copyPromptToClipboard(promptText);
    });
  }
}
// 將功能分離到不同的模組
const ModalModule = {
  show(nodeDetail) {
    // 顯示邏輯
  },
  
  generateContent(fullPrompt) {
    // 內容生成邏輯
  },
  
  bindEvents(content) {
    // 事件綁定邏輯
  }
};
const CopyModule = {
  bindButton(content) {
    // 複製按鈕綁定邏輯
  },
  
  copyToClipboard(text) {
    // 複製到剪貼簿邏輯
  }
};
// 測試內容生成
describe('generateModalContent', function() {
  it('should generate correct HTML structure', function() {
    const prompt = 'Test prompt';
    const result = generateModalContent(prompt);
    
    expect(result).toContain('node-info');
    expect(result).toContain('copy-button');
    expect(result).toContain('Test prompt');
  });
});
// 測試事件綁定
describe('bindCopyButtonEvent', function() {
  it('should bind click event to copy button', function() {
    const mockContent = {
      querySelector: jest.fn().mockReturnValue({
        addEventListener: jest.fn()
      })
    };
    
    bindCopyButtonEvent(mockContent);
    
    expect(mockContent.querySelector).toHaveBeenCalledWith('.copy-button');
  });
});
今天的開發讓我深刻體會到兩個重要的智慧:內容為王和簡化之美。
首先,內容為王:完成了 65 個節點的完整提示詞撰寫,這才是 GASO 專案真正的核心價值。沒有高品質的內容,再好的介面設計也沒有意義。
其次,簡化之美:在軟體開發中,我們常常陷入「功能越多越好」的迷思,但實際上,真正的價值在於解決使用者的核心問題,而不是提供所有可能的功能。
在 GASO 的開發旅程中,我們學會了在功能完整性和使用者體驗之間找到平衡點。有時候,移除功能比添加功能更能提升產品的價值。
簡化的智慧,不在於做得少,而在於做得對。
如果想要看一些我鐵人賽之外的 Google Apps Script 分享,
也歡迎追蹤我的 Threads 和 Facebook